/*
 *	----------YuBingUtils----------
 * 	
 *   author : YuBing  羽冰
 *   ver    : 2.5.2
 *   time   : 2020.9.12
 *   change : 修改了ajax发送请求的bug
 *            增加了批量进行事件委托的功能
 * 
 *  下次修改：这个cookie的操作当初设置也太cao了吧，改一下改一下，针对输入的参数数量来进行设置
 *           顺便使用说明也改一下，返回的cookie的格式，不然太久不用都忘了
 *           这个cookie传入的设置时间也修改一下吧，传入数组太麻烦了，还是传字符串自在
 * 
 *           顺便，倒计时也改成字符串传入吧
 * 
 *           这个模板引擎功能不够啊，还要分离开才能用，翻资料补全一下功能，看看能不能增加在html
 *           界面
 *           里也可以弄出来使用的
 *          
 *          顺便试试把数据结构的封装实现了吧，nice
 * 
 *          这个each和reduce的重构，好像还有点问题，重新审查修改一下
*/

var YuBingUtils = {}

/* 对象判定工具，判定指定元素/数据是否是对象 */
YuBingUtils.isObject = function (data) {
    return typeof data === "object" && !(data instanceof Array) && data.constructor === Object;
}

/* 数组判定工具，判定指定元素/数据是否是数组 */
YuBingUtils.isArray = function (data) {
    return typeof data === "object" && (data instanceof Array) && data.constructor === Array;
}

/* 判断传入的东西里面是不是空的 */
YuBingUtils.isEmpty = function (target) {
    if (Array.isArray(target) || target.constructor === HTMLCollection || target.constructor === NodeList) {
        // 普通数组、dom元素数组、节点数组
        // 长度为0，是空的
        if (target.length == 0) return true;
        return false;
    }
    else if (YuBingUtils.isObject(target)) {
        // 对象
        // 可以遍历到东西，不是空的
        for (var attr in target) return false;
        return true;
    }
    else if (target.entries) {
        // Map和Set
        // 长度为0，是空的
        if (target.size == 0) return true;
        return false;
    }
    else {
        // 都不是
        throw "Wrong target type";
    }
}

/* 
 * 字符串模板引擎
 * 反正模板引擎也是ES6的，直接用ES6了    
 * 变量设置 :
 *  template_selector : 内容包含有字符串模板的dom结构
 *        data        : 需要向字符串模板（<%=%>）中填入的数据
 *        data格式需求 : [{内容标识："要填入的内容"},{内容标识："要填入的内容"},···]
 *                       例如：<%=live%>，内容标识就为live，则data为：[{live："value"}]
*/
YuBingUtils.template = (template_selector, data) => {
    var template_str = document.querySelector(template_selector).innerHTML;
    /* 正则处理字符串，js代码暴露出来，html结构变为 print（）包裹，最后整体被print（）包裹; */
    /* 1. <%=%> */
    template_str = template_str.replace(/<%=([\s\S]*?)%>/g, `\`);  print($1); print(\``);
    /* 2. <%%> */
    template_str = template_str.replace(/<%([\s\S]*?)%>/g, `\`); $1 \n print(\``)
    template_str = `print(\` ${template_str}\`)`;
    let html = "";
    /* 
     * print是处理后包裹着html结构的东西 ↑ 
     * 将处理好的东西变为字符串放到变量html上 
    */
    let print = str => {
        html += str;
    };
    /* 
     * 最后处理的结果是一个print（）包裹的字符串
     * 将处理结果调用解释器重新编译解释
     * 也就相当于处理结果其中的HTML结构会运行上面定义好的print,而js代码会重新自动运行
     * 字符串中所有东西运行完毕的最后的最后的结果就是放在变量html里的字符串，就是一个包含了完整数据和结构的HTML结构 
    */
    eval(template_str);
    /* 输出字符串模板，在外面任意接收，然后设置innerHTML=返回值即可 */
    return html;
}
/* 
 * 对模板引擎原理的更进一步详细说明
 * 一般模板字符串都是在一个script标签中，将标签的type单独修改成 text/html后在里面定义好模板字符串
 * 然后这里输入的template_selector其实就是选中这个特殊script标签，并获取其中的内容
 * 假设模板字符串是：
 * <ul>
        <% for(var i = 0 ; i < data.length ; i ++) {%>
        <li>
            <img src="XXXX" alt="">
            <span> <%= data[i].name %></span>
        </li>
        <% } %>
    </ul>
 * 则经过首次处理后的结果就为：
 * print(`
 *  print(`<ul>`)
        for(var i = 0 ; i < data.length ; i ++) {
            print(`<li>`)
            print(`<img src="XXXX" alt="">`)
            print(`<span>`) 
            print(data[i].name) 
            print(`</span>`)
            print(`</li>`)
        } 
    print(`</ul>`) `)
 * 之后就调用编辑器将其重新编译运行，运行完毕后就变成了局部变量html中的字符串
 * 这个字符串就包含了完整的html结构和完整的数据，之后再将返回值直接赋值给指定的dom的innerHTML中即可 
 */

/* 遍历的重构
 * 参数设置 ：
 *    list : 要进行遍历的东西
 *    func : 数据操作函数
 *           函数第一个接收的参数是数值，第二接收的参数是下标/属性名:
 *              func(item,index/attr)
 *           根据需求选择接收
*/
YuBingUtils.each = function (list, func) {
    if (Array.isArray(list) || list.constructor === HTMLCollection || list.constructor === NodeList) {
        // 遍历普通数组、dom元素数组、节点数组
        for (var i = 0; i < list.length; i++) {
            func(list[i], i);
        }
    } else if (list.entries) {
        // 遍历Map和Set
        list.forEach(func);
    } else {
        // 遍历对象
        for (var attr in list) {
            func(list[attr], attr);
        }
    }
}

/* 累计器的重构
 *   参数设置 ：
 *    target : 要累计的目标对象
 *     func  : 数据处理函数，需要将数据处理结果返回
 *              可选接收参数 ： func(value,item,i)
 *              其中 : value为累计结果，item为当前正在处理的数据，i为当前数据的下标/key
 *     value : 累计值，可设置初始值
 *  注意事项  : 1. 只能累计数组、对象、Map、Set的Number类型和String类型
 *             2. 有返回值，返回值为累计结果
 */
YuBingUtils.reduce = function (target, func, value) {
    if (!target) throw "target can't be null";
    if (!func) throw "Data handler can't be null";

    if (YuBingUtils.isArray(target)) {
        // 如果是数组
        if (!value) value = type(target[0]);
        for (var i = 0; i < target.length; i++) {
            value = func(value, target[i], i);
        }
    }
    else if (YuBingUtils.isObject(target)) {
        // 如果是对象
        if (!value) value = type(Object.values(target)[0]);
        for (var attr in target) {
            value = func(value, target[attr], attr);
        }
    }
    else if (target.entries) {
        // 如果是Map或者Set
        if (!value) value = type(target.values().next().value);
        target.forEach(function (item, index) {
            value = func(value, item, index);
        })
    } else {
        // 两者都不是
        throw "Wrong target type";
    }
    return value;
}
/* --------这只是专门给reduce用的而已----------- */
function type(value) {
    var type = typeof value;
    switch (type) {
        case "string":
            value = "";
            break;
        case "number":
            value = 0;
            break;
        default:
            value = value;
            break;
    }
    return value;
}

/* 对象的深复制 
 *   参数设置 : 
 *   source  : 源对象
 *   target  : 累计器，不用设置值，为了能递归设置的
 *   返回值   : 克隆好的对象
*/
YuBingUtils.cloneObject = function (source, target) {
    var list = ["number", "string", "boolean", "undefined", "null", "function"];
    if (target === undefined) {
        // 初次调用，创建顶层父级
        // 判断源对象是否是HTML标签
        if (HTMLElement.prototype.isPrototypeOf(source)) {
            target = source.cloneNode(false);
        }
        else target = new source.constructor();
    }
    // 获取源对象中所有的属性名(可枚举和不可枚举的属性都会被返回)
    var names = Object.getOwnPropertyNames(source);
    // 遍历所有属性名
    for (var i = 0; i < names.length; i++) {
        // 获取源对象中自有属性的描述对象，描述对象中包含属性值、属性设置···
        // 自有属性-->不需要查找原型链就能获取的自己的属性
        var desc = Object.getOwnPropertyDescriptor(source, names[i]);
        // 判断该属性的值是否是基础数据类型
        if (list.includes(typeof desc.value)) {
            // 如果是基础数据类型，直接将该描述对象设置给目标对象的该属性
            Object.defineProperty(target, names[i], desc);
        } else {
            // 创建一个新引用型属性值
            var t;
            // 因为DOM是特殊不能使用new创建，因此判断该对象是否是DOM类型
            if (HTMLElement.prototype.isPrototypeOf(desc.value)) {
                t = desc.value.cloneNode(false);
                //   复制这个类型给这个对象
            } else {
                // 将属性值的类型分类判断
                switch (desc.value.constructor) {
                    case RegExp:
                        // 如果正则表达式时，需要将source和flags带入
                        t = new RegExp(desc.value.source, desc.value.flags);
                        break;
                    case Date:
                        // 日期属性需要将日期直接带入
                        t = new Date(desc.value);
                        break;
                    case Symbol:
                        // 直接创建Symbol，不能复制相同
                        t = Symbol();
                        break;
                    case Set:
                        // Set类型需要将values带入set
                        t = new Set(desc.value.values());
                        break;
                        break
                    case Map:
                        // map需要将entries带入map
                        t = new Map(desc.value.entries());
                        break;
                    default:
                        // 其他,包括数组和对象可以直接创建新的类别
                        t = new desc.value.constructor();
                        break;

                }
            }
            // 将描述对象的各种值设置给该对象的属性
            var o = {}
            o.value = t;
            if (desc.enumerable) o.enumerable = desc.enumerable;
            if (desc.writable) o.writable = desc.writable;
            if (desc.configurable) o.configurable = desc.configurable;
            if (desc.set) o.set = desc.set;
            if (desc.get) o.get = desc.get;
            Object.defineProperty(target, names[i], o);
            // 递归将获取的该属性值复制给创建的这个引用对象
            YuBingUtils.cloneObject(desc.value, t);
        }
    }
    return target;
}

/* 发送ajax请求
 * 参数设置： 
 *      ip : 要将数据发送到的地址,如果是get请求,要求自己将要携带的数据处理在ip上
 *    func : 服务器响应处理函数，需要设置形参接收服务器返回的数据
 *    data : 要携带发送的数据,如果要发送文件或表单数据需要自己预先处理
            如要发送表单数据，要自己预先处理==> data=new FormData(form),然后传入data
            文件同理 ：先用type为file的input选中文件,然后在file对象中选中上传的文件  
                        file.files类似文件数组,index为存储入其中的文件对象的下标
                        var file=document.querySelector("[type=file]");
                        data=file.files[index];
                        然后将 data传入
 *  methon : ajax请求方式， "GET" get方式请求，默认   "POST" post方式请求
 * setHead : 请求头，默认不设置，如果有需求可以设置,传入一个对象
 *              对象格式：{
 *                          header : string,
 *                          value  : string    
 *                      }
 *            其中，header和value的命名是固定的，而它们的值是string类型,且符合请求头规范
 * timeOut : ajax请求超时时间，默认不设置,传入数值类型,单位 ms
*/
YuBingUtils.ajaxSend = function (ip, func, data, methon, setHead, timeOut) {
    ip = ip || null;
    func = func || null;
    methon = methon || "GET";
    data = methon === "POST" ? JSON.stringify(data) : ((data) ? data : "");
    setHead = setHead || null;
    timeOut = timeOut || null;

    if (!ip) throw "ip can't be null";
    if (!func) throw "callback function can't be null";

    // 创建ajax对象
    var ajax = new XMLHttpRequest();
    // 创建监听
    ajax.addEventListener("readystatechange", function () {
        // 下载操作完成，且服务器返回了成功响应
        if (ajax.readyState === 4 && ajax.status === 200) func(ajax.response);
        // 下载操作完成，但是服务器没有返回成功响应
        else if (ajax.readyState === 4) throw "Request was sent, but the server didn't respond";
    });
    // 请求失败时
    ajax.addEventListener("error", function () { throw "Fail to send request"; });
    // 打开通信
    ajax.open(methon, ip);
    if (setHead) {
        if (!YuBingUtils.isObject(setHead)) throw "setHead must be object";
        ajax.setRequestHeader(setHead.header, setHead.value);
    }
    if (timeOut) ajax.timeout = timeOut;
    // 发送信息
    ajax.send(data);
}

/* 用ajax加载本地文件 
 *   参数配置  ： 
 *     Path   : 文件路径
 *     func   : 响应处理函数
 *   注意事项  : 响应处理函数必须设置一个形参接收加载响应
 *              传入的加载响应是未经处理过的响应，因此要自己在函数中处理
 * 加载使用案例: 1.JSON文件 : 
 *                      var obj=JSON.parse(xhr.response);
                        document.documentElement.style.backgroundColor=obj.f;
                2.XML文件 : 
                        var sprite=xhr.responseXML.querySelector("[n=enemy4_fly_1]");
                        console.log(sprite);
 *              以上仅为使用举例，具体如何使用依据需求决定
 */
YuBingUtils.ajaxLoad = function (Path, func) {
    if (!Path) throw "File path can't be null";
    if (!func) throw "Data handler can't be null";
    // 创建ajax对象
    var ajax = new XMLHttpRequest();
    // 创建监听
    ajax.onload = function () { func(ajax.response); }
    ajax.onerror = function () { throw "Fail to load files"; }
    ajax.open("GET", Path);
    ajax.send();
}

/* 对cookie的操作 
 * 参数设置:
 *    type     : 类型，存储还是获取   set存储，get获取,默认set
 *    data     : 对象，要存储的信息的集合                       格式： {data:value,data:value···}
 *    time     : cookie的过期时间，若不输入cookie不会长期保存    格式: [2020,09,11,18,30,00]
 * 其他注意事项 : cookie要在服务器运行才能存储，单击打开html文件无法存储cookie
*/
/* 
    option={
        data,
        time
    }
*/
YuBingUtils.Cookies = function (data, time, type) {
    type = type || "set";
    time = time || "";
    if (type == "set") {
        // 设置cookie
        if (time) {
            // 年月日拼接 : 2020/08/11
            var date_ymd = time.slice(0, 3).join("/");
            // 时分秒的拼接 : 18:30:00;
            var date_hms = time.slice(3).join(":");
            var date_str = date_ymd + " " + date_hms;
            var date = new Date(date_str);
            time = date.toUTCString();
        }
        for (var prop in data) {
            var str = prop + "=" + data[prop] + (time ? ";expires=" + time : "");
            document.cookie = str;
        }
    }
    else if (type == "get") {
        // 获取cookie
        return document.cookie.split(/;\s*/).reduce((value, item) => {
            var arr = item.split("=");
            value[arr[0]] = isNaN(arr[1]) ? arr[1] : Number(arr[1]);
            return value;
        }, {});
    }
    else {
        // 输入了别的，直接报错
        throw "输入正确type操作";
    }
}

/* 对localStorage的操作
 * 参数设置：
 *     data     : 输入的数据，输入对象则存储数据，输入key值数组则按照key值取出/删除数据
 *                    对象格式：{key:value,key:value,···}
 *                    数组格式: ["key","key","key"···]
 *     type     : 操作类型 ， set 根据key存储值  get 根据key获取值  remove根据key删除值
 *  其他注意事项 ：如果要清除全部的localStorage，自己直接调用localStorage.clear()即可
*/
YuBingUtils.localStorage = function (data, type) {
    type = type || "set";
    if (!window.localStorage) throw "浏览器不支持localStorage";
    if (!data) throw "data can't be null/undefined";
    switch (type) {
        case "set":
            // 设置localStorage
            if (!YuBingUtils.isObject(data)) throw "请输入正确的data";
            for (var attr in data) localStorage[attr] = data[attr];
            break;
        case "get":
            // 获取localStorage
            if (!YuBingUtils.isArray(data)) throw "请输入正确的data";
            var res = {};
            var value = 0;
            for (var i = 0; i < data.length; i++) {
                value = localStorage[data[i]];
                res[data[i]] = value;
            }
            return res;
        case "remove":
            // 移除localStorage
            if (!YuBingUtils.isArray(data)) throw "请输入正确的data";
            for (var j = 0; j < data.length; j++) localStorage.removeItem(data[j]);
            break;
    }
}

/* 对sessionStorage的操作
 * 参数设置：
 *     data     : 输入的数据，输入对象则存储数据，输入key值数组则按照key值取出/删除数据
 *                    对象格式：{key:value,key:value,···}
 *                    数组格式: ["key","key","key"···]
 *     type     : 操作类型 ， set 根据key存储值  get 根据key获取值  remove根据key删除值
 *  其他注意事项 ：如果要清除全部的sessionStorage，自己直接调用sessionStorage.clear()即可
*/
YuBingUtils.sessionStorage = function (data, type) {
    type = type || "set";
    if (!window.sessionStorage) throw "浏览器不支持sessionStorage";
    if (!data) throw "data can't be null/undefined";
    switch (type) {
        case "set":
            // 设置sessionStorage
            if (!YuBingUtils.isObject(data)) throw "请输入正确的data";
            for (var attr in data) sessionStorage[attr] = data[attr];
            break;
        case "get":
            // 获取sessionStorage
            if (!YuBingUtils.isArray(data)) throw "请输入正确的data";
            var res = {};
            var value = 0;
            for (var i = 0; i < data.length; i++) {
                value = sessionStorage[data[i]];
                res[data[i]] = value;
            }
            return res;
        case "remove":
            // 移除sessionStorage
            if (!YuBingUtils.isArray(data)) throw "请输入正确的data";
            for (var j = 0; j < data.length; j++) sessionStorage.removeItem(data[j]);
            break;
    }
}

/* 
 * webSocket的客户端操作
 *  参数配置:
 *      option: 对websocket的连接配置和操作,格式如下:
 *              {
 *                  ip,    --->连接地址,格式: ws://ip:port/path
 *                                       如: ws://localhost:8080
 *                handler  --->接收到服务端响应数据后的数据处理函数
 *             }
 * 
 *  返回值: webSocket实例对象 ws
 * 
 *  其他注意事项: 
 *      1. 接收到返回值ws后,发送信息用send方法,关闭连接用close方法:
 *              ws.send([sendSomeThing]);
 *              ws.close()
 */
/* 关于webSocket协议建立的一些概念看这里: 
 * ws/wss只能由客户端发起请求,他们本质上还是基于Http/Https请求的,
 * 首先在客户端由Http/Https发起请求,当服务器接受请求之后,
 * 原本用于请求的Http/Https协议就会被更改为webSocket协议
 * 然后就可以一直连接通信了
 */
YuBingUtils.webSocketLink = function (option) {
    var _ip = option.ip;
    var handler = option.handler;

    // 连接websocket
    var ws = new WebSocket(_ip);

    // 连接成功时
    ws.addEventListener("open", function () {
        console.log("连接成功");
    });

    // 连接关闭时
    ws.addEventListener("close", function () {
        console.log("连接关闭");
    });

    // 接收到消息时
    ws.addEventListener("message", function (evt) {
        // 对消息进行处理
        var res;
        try {
            res = JSON.parse(evt.data);
        } catch (err) {
            res = evt.data;
        }
        handler(res);
    });

    // 出错时
    ws.addEventListener("error", function (evt) {
        throw ("webSocketError: ", evt);
    });

    return ws;
}

/* 倒计时 
 * 参数设置:
 * end_time : 目标时间,是一个数组,格式 --> [2020,08,11,18,30,00]
 * func     : 倒计时处理函数
*/
/* 可能还需要修改一下，不传入日期只传入需要倒计时的时长什么的，比如说输入30s就倒计时30s */
YuBingUtils.countDown = function (end_time, func) {
    // 日
    var d = 0;
    // 时
    var h = 0;
    // 分
    var m = 0;
    // 秒
    var s = 0;
    // 毫秒
    var ms = 0;
    // 总体数据
    var times = {};

    if (end_time == null) throw "请输入目标时间";
    if (func == null) throw "请输入倒计时处理函数";

    // 年月日拼接 : 2020/08/11
    var date_ymd = end_time.slice(0, 3).join("/");
    // 时分秒的拼接 : 18:30:00;
    var date_hms = end_time.slice(3).join(":");
    var date_str = date_ymd + " " + date_hms;
    var end_time_d = new Date(date_str);
    // 目标时间的时间戳;
    var end_time_t = end_time_d.getTime();
    var t = setInterval(function () {
        // 获取当前时间;
        var now_t = Date.now();
        // 计算目标和当前时间的时间戳差值;
        var reduce = end_time_t - now_t;
        // 如果时间戳的差值已经小于0了表示当前时间已经超过了目标时间 ， 那么没有必要倒计时;
        if (reduce <= 0) {
            clearInterval(t);
            throw "当前时间已经超过了目标时间";
        }

        m = Math.floor(reduce / 60000 % 60)
        s = Math.round(reduce / 1000 % 60)
        ms = reduce % 1000;

        times = {
            min: m,
            sec: s,
            ms: ms
        }
        // 当前的倒计时是不是带天数的;
        // 如果大于了 86400000则表示大于 1 天;
        if (reduce > 864e+5) {
            // 计算应该带有天数;
            d = Math.floor(reduce / 864e+5);
            h = Math.floor(reduce / 36e+5 % 24);
            times.days = d;
            times.hours = h;
        } else {
            // 计算时没有天数;
            h = Math.floor(reduce / 36e+5);
            times.hours = h;
        }

        // 倒计时数据处理函数，可以用倒计时获取的数据做些操作
        // 比如说到倒计时到某个点的时候操作什么东西
        func(times);

        // 到点关闭倒计时
        if (d <= 0 && h <= 0 && m <= 0 && s <= 0 && ms <= 0) clearInterval(t);
    }, 1000)
}

/* 随机颜色 
 * 参数配置:
 * isRGBA : true/false  用RGBA的随机颜色还是16进制的随机颜色，默认为16进制
*/
YuBingUtils.randomColor = function (isRGBA) {
    if (isRGBA) {
        // RGBA的随机颜色
        var RGBA_Random_Color;
        var r;
        var g;
        var b;
        var a;
        do {
            r = parseInt(Math.random() * 255);
            g = parseInt(Math.random() * 255);
            b = parseInt(Math.random() * 255);
            a = Math.random().toFixed(1);
        } while (r == 0 || g == 0 || b == 0 || a == 0);
        RGBA_Random_Color = "rgba(" + r + "," + g + "," + b + "," + a + ")";
        return RGBA_Random_Color;
    }
    // 16进制的随机颜色
    var random_color_16 = "#";
    for (var i = 0; i < 6; i++) random_color_16 += parseInt(Math.random() * 16).toString(16);
    return random_color_16;
}

// 获取任意月份的最后一天
YuBingUtils.getFinalDay = () => {
    // 当前年份
    let year = new Date().getFullYear();
    // 下个月
    let nextMonth = new Date().getMonth() + 2;
    // 下个月第一天
    let nextOneDay = year + "-" + nextMonth + "-1";
    // 一天的时间戳
    let onDay = 86400000;
    // 上个月最后一天
    return (new Date(nextOneDay).getTime() - onDay);
}

export default YuBingUtils;